Skip to content

feat: organization avatar upload (set/update/remove)#769

Open
softmarshmallow wants to merge 7 commits into
mainfrom
feat/org-avatar-upload
Open

feat: organization avatar upload (set/update/remove)#769
softmarshmallow wants to merge 7 commits into
mainfrom
feat/org-avatar-upload

Conversation

@softmarshmallow

@softmarshmallow softmarshmallow commented Jun 3, 2026

Copy link
Copy Markdown
Member

What

Lets an organization member set, replace, or remove their organization's avatar from the existing Organization → Settings → General form. No new routes or pages.

PRD (summary)

  • Where: a new "Organization avatar" control at the top of the existing General settings form (/organizations/[name]/settings/profile). Reuses the form's single Save.
  • Interaction: Change (file picker + instant local preview) / Remove / initials fallback when empty. Persists on Save alongside the other profile fields.
  • Accepted: PNG / JPEG / WebP, ≤ 2 MB. No cropper — object-cover at render. Replace overwrites in place; Remove clears it.
  • Permissions: member-level, matching the existing profile edit (only Delete stays owner-only).
  • Shows after: the org switcher (sidebar + org menu) — already wired to avatar_url — plus the new settings preview.

Approach — server action + RLS end to end (no service_role)

The profile submit goes through a "use server" action (actions.ts) instead of the old private API route. Every supabase call — the org row read/update and the avatar object upload/remove — runs through the same user-authed, RLS-aware createClient(). There is zero service_role in this feature; RLS is the authorization boundary.

  • updateOrganizationProfile(organization_name, formData) reads + updates the organization row with the user-authed client (the organization SELECT/UPDATE membership policy rls_organization is the gate; the org select is kept only for a friendly error).
  • The avatar object is uploaded/removed via client.storage.from("avatars") — authorized by the new storage policies below.
  • Avatar stored at {organization_id}/avatar (folder-first so the policy can parse the org id), upsert: true. Only avatar_path is persisted; the public URL is computed at read time via PublicUrls.organization_avatar_url.
  • revalidatePath on the settings page after the mutation.

New migration — supabase/migrations/20260604120000_avatars_bucket_rls.sql

  • Creates the avatars bucket idempotently (insert … on conflict (id) do nothing, public) — a no-op in prod where it already exists; present for local-stack parity.
  • storage.objects policies for bucket_id = 'avatars', scoped to org membership via public.rls_organization((storage.foldername(name))[1]::bigint) — mirroring the existing storage / www bucket policy shape. Covers INSERT, UPDATE (upsert), DELETE (remove). SELECT stays public to match the public-URL read.

No DB column migration: organization.avatar_path already existed.

Reconciliation notes (prod avatars bucket is untracked)

  • The bucket already exists in prod (created via dashboard) → the insert is on conflict do nothing.
  • Each policy is drop policy if exists … first, so the migration coexists with any same-named policy already on the prod bucket and is safe to (re)apply.
  • The bucket is shared with user_profile avatars (uuid-keyed paths). A blanket ::bigint cast would raise on those — so the membership check is wrapped in a CASE (guaranteed eval order): a non-numeric first segment yields false instead of erroring, leaving those objects to whatever policy governs them.

Files touched

  • new …/settings/profile/actions.ts — the server action (user-auth client only).
  • new …/settings/profile/avatar-field.tsx — client control (pick + preview + remove).
  • mod …/settings/profile/page.tsx — render current avatar, wire form to the bound action.
  • del editor/app/(api)/private/accounts/organizations/[org]/profile/route.ts — the now-dead route.
  • new supabase/migrations/20260604120000_avatars_bucket_rls.sql — bucket + RLS.
  • new …/settings/profile/__tests__/actions.test.ts — unit tests.

Verification

  • pnpm lint (oxlint) — clean.
  • pnpm turbo typecheck --filter=editor35/35 pass.
  • pnpm vitest run …/actions.test.ts6/6 pass: membership gate, type + 2 MB validation, upload via the user-auth client to {id}/avatar (upsert), and remove (object delete + avatar_path: null). The tests assert no service_role path is used.
  • Pre-commit lefthook (oxlint + oxfmt) on every commit.

Local-env caveat (migration not live-applied)

The RLS policy could not be exercised against a live DB here: no Supabase CLI and Docker is unavailable, so supabase db reset / a real upload can't run. Proof is therefore: the migration SQL (modeled directly on the in-repo storage/www precedents), the updated unit tests (which now assert the user-auth storage path), and typecheck. A reviewer with a local stack can supabase db reset and exercise the round-trip end to end.

Notes / follow-ups

  • Stable key + upsert means the public URL is unchanged after a replace, so a cached avatar may need a hard refresh. Trivial cache-busting follow-up if desired.

Summary by CodeRabbit

  • New Features

    • Organization profile form now updates display name, email, description, blog and avatar; includes local preview and remove support, and uses the new server-side update flow.
    • Avatar uploads accept PNG/JPEG/WebP and are limited to 2MB.
  • Tests

    • Added tests for profile updates, membership gating, avatar upload/validation/remove flows, and cache revalidation.
  • Chores

    • Added storage for organization avatars with access controls and increased server upload limit to support ~2MB avatars.

@vercel

vercel Bot commented Jun 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
grida Ready Ready Preview, Comment Jun 4, 2026 8:16am
6 Skipped Deployments
Project Deployment Actions Updated (UTC)
backgrounds Ignored Ignored Preview Jun 4, 2026 8:16am
blog Ignored Ignored Preview Jun 4, 2026 8:16am
code Ignored Ignored Jun 4, 2026 8:16am
docs Ignored Ignored Preview Jun 4, 2026 8:16am
legacy Ignored Ignored Jun 4, 2026 8:16am
viewer Ignored Ignored Preview Jun 4, 2026 8:16am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a7df392-56c6-4c64-bba4-8a021d00ae84

📥 Commits

Reviewing files that changed from the base of the PR and between 675cd40 and db37bcd.

📒 Files selected for processing (7)
  • editor/app/(api)/private/accounts/organizations/[org]/profile/route.ts
  • editor/app/(site)/organizations/[organization_name]/settings/profile/__tests__/actions.test.ts
  • editor/app/(site)/organizations/[organization_name]/settings/profile/actions.ts
  • editor/app/(site)/organizations/[organization_name]/settings/profile/avatar-field.tsx
  • editor/app/(site)/organizations/[organization_name]/settings/profile/page.tsx
  • editor/next.config.ts
  • supabase/migrations/20260604120000_avatars_bucket_rls.sql
💤 Files with no reviewable changes (1)
  • editor/app/(api)/private/accounts/organizations/[org]/profile/route.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • editor/next.config.ts
  • supabase/migrations/20260604120000_avatars_bucket_rls.sql
  • editor/app/(site)/organizations/[organization_name]/settings/profile/page.tsx
  • editor/app/(site)/organizations/[organization_name]/settings/profile/avatar-field.tsx
  • editor/app/(site)/organizations/[organization_name]/settings/profile/tests/actions.test.ts
  • editor/app/(site)/organizations/[organization_name]/settings/profile/actions.ts

Walkthrough

Profile updates moved from an API route to a Next.js Server Action that handles auth, RLS authorization, text-field updates, avatar upload/removal to an {org.id}/avatar storage path, page wiring to the server action, a client avatar component for preview/removal, tests, Next config to raise Server Action size, and a Supabase migration to add an avatars bucket with RLS.

Changes

Organization Profile Update with Avatar Management

Layer / File(s) Summary
Server Action: Profile update with avatar validation and storage
editor/app/(site)/organizations/[organization_name]/settings/profile/actions.ts
updateOrganizationProfile server action authenticates and authorizes via RLS org lookup, validates avatar MIME type and size (≤2MB), uploads/removes avatar at {org.id}/avatar in avatars with upsert, updates organization fields (conditionally including avatar_path), and calls revalidatePath for the profile settings route.
Server Action test suite: Auth, field updates, avatar validation
editor/app/(site)/organizations/[organization_name]/settings/profile/__tests__/actions.test.ts
Vitest suite mocking Supabase createClient and next/cache (revalidatePath), stubbing a user client to exercise membership gating, text-only updates, avatar upload to {ORG_ID}/avatar with upsert and contentType sniffing, avatar removal, spoofed MIME/unsupported-bytes rejection, file-size limit rejection, and required-field validation.
Avatar field component: Client-side preview, selection, and removal
editor/app/(site)/organizations/[organization_name]/settings/profile/avatar-field.tsx
OrganizationAvatarField displays current avatar or initials fallback, restricts selectable types to PNG/JPEG/WebP, shows local preview via URL.createObjectURL, revokes blob URLs, and emits remove_avatar=1 when removing the avatar.
Profile page wiring: Server action binding and avatar component integration
editor/app/(site)/organizations/[organization_name]/settings/profile/page.tsx
Profile settings page imports updateOrganizationProfile, PublicUrls, and OrganizationAvatarField, derives avatar_url from avatar_path, binds the form to the server action, and passes current_avatar_url and display_name to the avatar component.
Next config: serverActions bodySizeLimit
editor/next.config.ts
Sets nextConfig.experimental.serverActions.bodySizeLimit to "3mb" to allow ~2MB avatar uploads plus multipart overhead.
Supabase migration: avatars bucket and RLS policies
supabase/migrations/20260604120000_avatars_bucket_rls.sql
Migration creates a public avatars storage bucket idempotently and installs/drops-and-recreates RLS policies for public SELECT and org-member INSERT/UPDATE/DELETE by validating the first path segment as the organization id via public.rls_organization(...).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: organization avatar upload (set/update/remove)' directly and specifically summarizes the main change: adding organization avatar functionality with set, update, and remove operations to the profile settings form.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/org-avatar-upload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2908c13a97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

import { createClient, service_role } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";

const AVATAR_MAX_BYTES = 2 * 1024 * 1024; // ~2MB

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect Next's server-action upload limit

For avatars between 1MB and 2MB, this limit says the file is accepted, but the form now posts through a Server Action and I checked editor/next.config.ts: it does not configure experimental.serverActions.bodySizeLimit, so Next rejects the request at its default 1MB limit before updateOrganizationProfile runs. Either lower this validation to the framework limit or raise the Next config; otherwise users choosing a “valid” 1–2MB image get a framework 413 instead of an avatar update.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@editor/app/`(site)/organizations/[organization_name]/settings/profile/actions.ts:
- Around line 57-63: The current check relies only on the client-provided
avatar.type (and later forwards it as contentType), so add server-side MIME
sniffing: read the first bytes via await avatar.arrayBuffer(), inspect magic
bytes/signature to detect actual file type (e.g., PNG/JPEG/GIF signatures) and
verify it matches an allowed type from AVATAR_ACCEPTED_TYPES and
AVATAR_MAX_BYTES; if the sniffed type and declared avatar.type disagree or the
signature is not an accepted image type, throw an error and do not forward the
client contentType to the upload — instead set the upload contentType based on
the sniffed/validated type before storing to the avatars bucket.
- Around line 86-95: The current update uses String(display_name)/String(email)
which will store the literal "null" if formData.get returned null; change to
explicitly validate and guard required fields before calling
client.from("organization").update: ensure display_name and email are non-null
(e.g., if (!display_name || !email) return an error/throw or set update to
abort) and then use the same guarded conversion pattern as description/blog
(display_name ? String(display_name) : undefined and email ? String(email) :
undefined) so you never call String(null); update the code around the
client.from("organization").update call and the variables display_name/email
handling to perform this check and early return.

In
`@editor/app/`(site)/organizations/[organization_name]/settings/profile/avatar-field.tsx:
- Around line 28-39: The code currently creates blob URLs in onPick via
URL.createObjectURL and never revokes them; update onPick to revoke any existing
preview URL before setting a new one (use the current preview state), update
onRemove to revoke the current preview URL before clearing it, and add a
useEffect cleanup that revokes the preview URL on unmount; reference the onPick
and onRemove handlers and the preview state variable so you revoke URL(s)
whenever preview changes or component unmounts.

In
`@editor/app/`(site)/organizations/[organization_name]/settings/profile/page.tsx:
- Around line 69-72: OrganizationAvatarField is being passed data.display_name
which can be null, but its display_name prop is typed as string; update the prop
contract or pass a safe value. Either change OrganizationAvatarField's prop type
for display_name to accept string | null | undefined (and keep its internal
checks like display_name?.charAt(0)), or coerce at the callsite by passing
display_name={data.display_name ?? undefined}. Update the TS type for
OrganizationAvatarField (and any related props/interfaces) or adjust this call
accordingly so types align.
- Around line 61-65: The profile form used by updateOrganizationProfile is
missing multipart encoding, so avatar File uploads won’t be sent correctly.
Update the form in profile/page.tsx by adding encType="multipart/form-data" to
the form element that wraps the avatar inputs, keeping the existing id and
action binding intact, and verify upload/removal flows still work through the
updateOrganizationProfile server action.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e95bd25-59da-4454-860e-92a26f5e99b8

📥 Commits

Reviewing files that changed from the base of the PR and between 8faec98 and 2908c13.

📒 Files selected for processing (5)
  • editor/app/(api)/private/accounts/organizations/[org]/profile/route.ts
  • editor/app/(site)/organizations/[organization_name]/settings/profile/__tests__/actions.test.ts
  • editor/app/(site)/organizations/[organization_name]/settings/profile/actions.ts
  • editor/app/(site)/organizations/[organization_name]/settings/profile/avatar-field.tsx
  • editor/app/(site)/organizations/[organization_name]/settings/profile/page.tsx
💤 Files with no reviewable changes (1)
  • editor/app/(api)/private/accounts/organizations/[org]/profile/route.ts

Comment on lines +69 to +72
<OrganizationAvatarField
current_avatar_url={avatar_url}
display_name={data.display_name}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

display_name may be null, but the prop type requires string.

The Input for display name uses data.display_name ?? undefined (Line 87), implying the column is nullable, yet here it is passed into OrganizationAvatarField's display_name: string prop. The component already defends with display_name?.charAt(0), so widen the prop type to string | null | undefined (or coerce here) to keep the contract honest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@editor/app/`(site)/organizations/[organization_name]/settings/profile/page.tsx
around lines 69 - 72, OrganizationAvatarField is being passed data.display_name
which can be null, but its display_name prop is typed as string; update the prop
contract or pass a safe value. Either change OrganizationAvatarField's prop type
for display_name to accept string | null | undefined (and keep its internal
checks like display_name?.charAt(0)), or coerce at the callsite by passing
display_name={data.display_name ?? undefined}. Update the TS type for
OrganizationAvatarField (and any related props/interfaces) or adjust this call
accordingly so types align.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
supabase/migrations/20260604120000_avatars_bucket_rls.sql (1)

35-85: ⚡ Quick win

RLS lookup indexing for public.rls_organization()

public.rls_organization(p_organization_id) checks organization_member with om.organization_id = p_organization_id and om.user_id = auth.uid(). The migrations already add indexes on organization_member(organization_id) and organization_member(user_id). For best performance on this paired predicate, consider adding a composite index on (organization_id, user_id).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260604120000_avatars_bucket_rls.sql` around lines 35 -
85, The RLS helper public.rls_organization(p_organization_id) queries
organization_member filtering by om.organization_id = p_organization_id and
om.user_id = auth.uid(), so add a composite index to speed that paired lookup:
create an index on organization_member(organization_id, user_id) in the
migrations (alongside the existing single-column indexes) so
public.rls_organization and the storage.objects RLS policies (which call
public.rls_organization) use the composite index for efficient lookups.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/migrations/20260604120000_avatars_bucket_rls.sql`:
- Around line 3-20: The migration comment incorrectly states non-numeric
(user_profile/UUID) avatar objects “fall through” to other policies, but the
migration only creates the public SELECT and organization-scoped
INSERT/UPDATE/DELETE policies for bucket 'avatars' (scoped to the
`{organization_id}/avatar` path via the numeric-first-segment CASE and using the
public.rls_organization() helper), so either add explicit RLS policies to allow
UUID-keyed user_profile writes/removals to storage.objects where bucket_id =
'avatars' or update the comment to remove the claim; locate the migration SQL
that inserts policies for bucket 'avatars' and either (A) create matching
INSERT/UPDATE/DELETE policies that permit paths with non-numeric first segments
(e.g., UUIDs) for user_profile actors, or (B) change the descriptive text to
state that only org-scoped `{organization_id}/avatar` writes are permitted and
user_profile avatars are not covered.

---

Nitpick comments:
In `@supabase/migrations/20260604120000_avatars_bucket_rls.sql`:
- Around line 35-85: The RLS helper public.rls_organization(p_organization_id)
queries organization_member filtering by om.organization_id = p_organization_id
and om.user_id = auth.uid(), so add a composite index to speed that paired
lookup: create an index on organization_member(organization_id, user_id) in the
migrations (alongside the existing single-column indexes) so
public.rls_organization and the storage.objects RLS policies (which call
public.rls_organization) use the composite index for efficient lookups.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60e7426d-e2fc-413d-995c-a7bcdc2dac6c

📥 Commits

Reviewing files that changed from the base of the PR and between 2908c13 and 0c82d73.

📒 Files selected for processing (3)
  • editor/app/(site)/organizations/[organization_name]/settings/profile/__tests__/actions.test.ts
  • editor/app/(site)/organizations/[organization_name]/settings/profile/actions.ts
  • supabase/migrations/20260604120000_avatars_bucket_rls.sql

Comment thread supabase/migrations/20260604120000_avatars_bucket_rls.sql Outdated
Add an avatar control to the org General settings form. The profile submit
now goes through a "use server" action instead of the private API route:

- updateOrganizationProfile reads/updates the org row with the user-authed,
  RLS-aware client (the organization SELECT/UPDATE membership policy is the
  authorization gate); the avatar object write uses the service_role storage
  client because the avatars bucket has no tracked storage RLS.
- avatar-field.tsx: pick + local preview + remove, carried as the avatar file
  and a remove_avatar flag on the existing form.
- avatar stored at org-{id}/avatar (upsert); only avatar_path is persisted, the
  URL is computed at read time via PublicUrls. png/jpeg/webp, <=2MB.
- remove the now-dead /private/accounts/organizations/[org]/profile route.

No new routes/pages; reuses the avatars bucket + existing render sites.
Membership gate, avatar type/size validation, org-{id}/avatar upsert upload,
and the remove (avatar_path: null) path. Mocks the supabase clients and
next/cache.
Per owner: server actions must not use service_role — RLS is the boundary
end to end. The avatar upload/remove now go through the same user-authed
createClient() as the org row read/update; authorization is enforced by
storage policies, not a code-side gate.

- New migration 20260604120000_avatars_bucket_rls.sql:
  - idempotently creates the public 'avatars' bucket (no-op in prod, where it
    already exists; on conflict do nothing) for local parity.
  - adds org-membership storage.objects policies (insert/update/delete) scoped
    via public.rls_organization on the path's first segment, mirroring the
    storage/www bucket precedent. SELECT stays public for public-URL reads.
  - each policy is drop-if-exists guarded to coexist with any same-named prod
    policy; the membership check is CASE-guarded so non-numeric paths (the
    bucket is shared with uuid-keyed user_profile avatars) return false instead
    of raising on the ::bigint cast.
- Path scheme changed org-{id}/avatar -> {id}/avatar so the policy can parse
  the org id via (storage.foldername(name))[1].
- Tests assert the upload/remove go through the user-auth client (no
  service_role) and the {id}/avatar path.
- Reject missing display_name/email explicitly instead of persisting the
  literal string "null" via String(null).
- Sniff png/jpeg/webp from the file's leading bytes rather than trusting
  the client-supplied File.type; reject mismatches and use the sniffed
  type as the upload contentType.
- Extend tests: missing-required-field rejection, spoofed-MIME rejection,
  sniffed-type-wins, no-signature rejection.
The picked avatar File never reached the server action: the <form> lacked
encType="multipart/form-data", and Next 16's default server-action
bodySizeLimit (1MB) is below the 2MB avatar cap, 413-ing 1-2MB files before
the action runs.

- page.tsx: add encType="multipart/form-data" to the profile form.
- next.config.ts: set experimental.serverActions.bodySizeLimit to '3mb' to
  fit the 2MB cap plus multipart overhead.
- Add encType="multipart/form-data" to the profile form so the picked
  avatar File is actually encoded into the server action request.
- Revoke the createObjectURL preview on replace and unmount to stop the
  blob URL leak.
- Widen display_name prop to string | null | undefined (it's nullable at
  the call site).
These are the only write policies on the avatars bucket. Under
deny-by-default RLS, a non-numeric (e.g. uuid-keyed) path is simply
denied for insert/update/delete — it does not fall through to another
policy, because none exists. No user_profile avatar upload path exists
in the codebase today.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant